<?
import('japha.lang.String');
import('japha.util.Vector');
import('japha.util.Enumeration');
import('japha.io.PrintWriter');
import('japha.io.StringWriter');
import('japha.lang.reflect.*');

/**
 * A <code>TestSuite</code> is a <code>Composite</code> of Tests.
 * It runs a collection of test cases. Here is an example using
 * the dynamic test definition.
 * <pre>
 * TestSuite suite= new TestSuite();
 * suite.addTest(new MathTest("testAdd"));
 * suite.addTest(new MathTest("testDivideByZero"));
 * </pre>
 * Alternatively, a TestSuite can extract the tests to be automatically.
 * To do so you pass the class of your TestCase class to the
 * TestSuite constructor.
 * <pre>
 * TestSuite suite= new TestSuite(MathTest.class);
 * </pre>
 * This constructor creates a suite with all the methods
 * starting with "test" that take no arguments.
 *
 * @see Test
 * @author Ryan Gantt
 */
class TestSuite extends Object implements Test
{
	/**
	 * Vector
	 */
	private $fTests;

	/**
	 * String
	 */
	private $fName = '';

	public function __construct( $class = null )
	{
		$this->fTests = new Vector(10); 
		if( $class == null )
			return;
		else if( $class instanceof _Class )
		{
			$this->fName = $class->getName();
			try
			{
				$this->getTestConstructor( $class ); // Avoid generating multiple error messages
			}
			catch ( Exception $e ) // NoSuchMethodException
			{
				$this->addTest( $this->warning("Class " . $class->getName() . " has no public constructor TestCase(String name) or TestCase()") );
				return;
			}
			if ( !Modifier::isPublic( $class->getModifiers() ) )
			{
				$this->addTest( $this->warning("Class " . $class->getName() . " is not public") );
				return;
			}
			$superClass = $class;
			$names = new Vector();
			//TestImpl::initStaticClass('TestImpl');
			$clazz = _Class::forName('TestImpl');
			while ( $clazz->isAssignableFrom( $superClass ) )
			{
				$methods = $superClass->getDeclaredMethods();
				for ( $i = 0; $i < count( $methods ); $i++ )
				{
					if( preg_match( '/^test/', $methods[ $i ] ) )
						$this->addTestMethod( new Method( $class->getName(), $methods[ $i ] ), $names, $class );
				}
				$superClass = $superClass->getSuperclass();
			}
			if ( $this->fTests->size() == 0 )
				$this->addTest( $this->warning("No tests found in " . $class->getName() ) );
		}
		else
			$this->setName( $name );
	}

	/**
	 * Adds a test to the suite.
	 */
	public function addTest( Test $test )
	{
		$this->fTests->addElement( $test );
	}

	/**
	 * Adds the tests from the given class to the suite
	 */
	public function addTestSuite( _Class $testClass )
	{
		$this->addTest( new TestSuite( $testClass ) );
	}

	private function addTestMethod( Method $m, Vector $names, _Class $theClass )
	{
		$name = new String( $m->getName() );
		if ( $names->contains( $name ) )
			return;
		if ( !$this->isPublicTestMethod( $m ) )
		{
			if ( $this->isTestMethod( $m ) )
				$this->addTest( $this->warning("Test method isn't public: " . $m->getName() ) );
			return;
		}
		$names->addElement( new String( $name ) ); // wrap string as object
		$this->addTest( $this->createTest( $theClass, $name->toString() ) );
	}

	/**
	 * ...as the moon sets over the early morning Merlin, Oregon
	 * mountains, our intrepid adventurers type...
	 */
	public function createTest( _Class $theClass, $name )
	{
		// we're losing track of the $name variable in this method -- it doesn't carry over into the test object
		try
		{
			$constructor = $this->getTestConstructor( $theClass );
		}
		//catch ( NoSuchMethodException $e )
		catch ( Exception $e )
		{
			return $this->warning("Class " . $theClass->getName() . " has no public constructor TestCase(String name) or TestCase()");
		}
		
		$test = new Object();
		try
		{
			if ( count( $constructor->getParameterTypes() ) == 0)
			{
				$test = $constructor->newInstance( array( new Object ) ); // Object[0]
				//$test = $constructor->newInstance( array( $name ) ); // Object[0]
				if ( $test instanceof TestCase )
					$test->setName( $name );
			}
			else
				$test = $constructor->newInstance( array( $name ) ); // Object[]{ $name }
		}
		catch ( Exception $e )
		//catch ( InstantiationException $e )
		{
			return $this->warning('Cannot instantiate test case: '.$name.' ('.$this->exceptionToString( $e ).')');
		}
		/*
		catch ( InvocationTargetException $e )
		{
			echo 'invoc exp';
			return $this->warning('Exception in constructor: '.$name.' ('.$this->exceptionToString( $e->getTargetException() ).')');
		}
		catch ( IllegalAccessException $e )
		{
			echo 'illae exp';
			return $this->warning('Cannot access test case: '.$name.' ('.$this->exceptionToString( $e ).')');
		}
		*/
		return $test;
	}

	/**
	 * Converts the stack trace into a string
	 */
	private static function exceptionToString( Throwable $t )
	{
		$stringWriter = new StringWriter();
		$writer = new PrintWriter( $stringWriter );
		$t->printStackTrace( $writer );
		return $stringWriter->toString();
	}

	/**
	 * Counts the number of test cases that will be run by this test.
	 */
	public function countTestCases()
	{
		$count = 0;
		for ( $e = $this->tests(); $e->hasNext(); $e->next() )  // Was Enumeration, now Iterator
		{
			$test= $e->current();
			$count = $count + $test->countTestCases();
		}
		return $count;
	}

	/**
	 * Gets a constructor which takes a single String as
	 * its argument or a no arg constructor.
	 */
	public static function getTestConstructor( _Class $theClass ) // throws NoSuchMethodException
	{
		//Class[] $args = { String.class };
		//$args = String::getclass();
		$args = _Class::forName('String');
		try
		{
			return $theClass->getConstructor( $args );
		}
		catch (NoSuchMethodException $e) { /* fall through */ }
		return $theClass->getConstructor( new _Class() );
	}

	private function isPublicTestMethod( Method $m )
	{
		return ( $this->isTestMethod( $m ) && Modifier::isPublic( $m->getModifiers() ) );
	}

	private function isTestMethod( Method $m )
	{
		$name= new String( $m->getName() );
		//Class[] $parameters = $m->getParameterTypes();
		$parameters = $m->getParameterTypes(); //array
		$returnType = $m->getReturnType(); //instanceof _Class
		return ( count( $parameters ) == 0 && preg_match( '/^test/', $name->toString() ) ); //&& $returnType->equals( Void::TYPE ) );
	}

	/**
	 * Runs the tests and collects their result in a TestResult.
	 */
	public function run( TestResult $result = null )
	{
		if( $result == null || !( $result instanceof TestResult ) )
			throw new Exception('TestSuite::run requires a TestResult object as a parameter!');
		for ( $e = $this->tests(), $i = 0; $e->hasNext(); $e->next(), $i++ )
		{
			if ( !$result->shouldStop() )
				$this->runTest( $this->fTests->get( $i ), $result );
		}
	}

	public function runTest( Test $test, TestResult $result )
	{
		$test->run( $result );
	}

	/**
	 * Returns the test at the given index
	 */
	public function testAt( $index )
	{
		return $this->fTests->elementAt( $index ); // casted to Test
	}

	/**
	 * Returns the number of tests in this suite
	 */
	public function testCount()
	{
		return $this->fTests->size();
	}

	/**
	 * Returns the tests as an enumeration
	 */
	public function tests()
	{
		return new ListIterator( $this->fTests );
	}

	public function toString()
	{
		if ( $this->getName() != null )
			return $this->getName();
		return parent::toString();
	}

	/**
	 * Sets the name of the suite.
	 * @param name The name to set
	 */
	public function setName( $name )
	{
		$this->fName = $name;
	}

	/**
	 * Returns the name of the suite. Not all
	 * test suites have a name and this method
	 * can return null.
	 */
	public function getName()
	{
		return $this->fName;
	}

	/**
	 * Returns a test which will fail and log a warning message.
	 */
	private static function warning( $message )
	{
		$tc = new TestCaseImpl();
		$tc->setMessage( $message );
		return $tc;
	}
}

class TestCaseImpl extends TestCase
{
	public function setMessage( $message )
	{
		$this->message = $message;
	}

	public function runTest()
	{
		$this->fail( $this->message );
	}

	public function setUp(){}
	public function tearDown(){}
}